﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Windows;
using DeepEarth.Core;
using DeepEarth.Core.Collections;
using DeepEarth.Core.Utilities;
using System.Linq;
using System.Collections.Specialized; 

#if !WINDOWS_PHONE
using System.ComponentModel.Composition;
#endif

namespace DeepEarth.Map.Core.Tiling
{
	public abstract class BaseTileSourceProvider<T> : BaseTileSourceProvider where T : BaseTileSourceFactory, new()
	{
		protected BaseTileSourceProvider()
			: base(new T())
		{
		}

		protected BaseTileSourceProvider(MapMode mode)
			: this()
		{
			var realMode = TileSourceFactory.ValidateAndCoerceMapMode(mode);
			Mode = realMode;
		}
	}

#if !WINDOWS_PHONE
	[Export] 
#endif

	// Exposes a list of tile sources to be used by the map
	public abstract class BaseTileSourceProvider : DependencyObject, INotifyPropertyChanged 
	{
		protected BaseTileSourceProvider(BaseTileSourceFactory sourceFactory)
		{
			TileSourceFactory = sourceFactory;
			_tileSources = new AtomicObservableCollection<BaseTileSource>();
			_tileSources.CollectionChanged += OnTileSourcesCollectionChanged;
		}

		#region Properties

		// The Logical Bounds within which this overly has tiles
		// default to the whole world
		private Rect logicalBounds = new Rect(0, 0, 1, 1);
		public Rect LogicalBounds
		{
			get
			{
				return logicalBounds;
			}
			set
			{
				logicalBounds = value;

				foreach (BaseTileSource bts in this.TileSources)
				{
					bts.LogicalBounds = value;
				}
			}
		}

		// the maximum zoom level at which this overlay has data
		private double maxDataZoomLevel = double.MaxValue;
		public double MaxDataZoomLevel
		{
			get
			{
				return maxDataZoomLevel;
			}
			set
			{
				maxDataZoomLevel = value;

				foreach (BaseTileSource bts in this.TileSources)
				{
					bts.MaxDataZoomLevel = value;
				}
			}
		}

		// TODO: TileSourceProvider used to be known as TileSourceOverlay - you could set the mode on the overlay but it was
		// up to the map control to effect the tile source change. If this is now just a provider, perhaps the Map itself
		// should have a Mode property - this is a refactoring target.
		private MapMode mode;
		public MapMode Mode
		{
			get { return mode; }
			set
			{
				if (value == null) throw new ArgumentNullException("value");

				var newMode = TileSourceFactory.ValidateAndCoerceMapMode(value);
				var oldMode = mode;

				mode = newMode;
				ModeChanged(oldMode, newMode);
				NotifyPropertyChanged(() => Mode);
				NotifyPropertyChanged(() => ActiveTileSource);
			}
		}

		public BaseTileSource ActiveTileSource
		{
			get
			{
				return TileSourcesCollection.Where(t => t.Mode == Mode).FirstOrDefault() ;
			}
		}

		/// <summary>
		/// All the available modes
		/// </summary>
		public IList<MapMode> AvailableOverlayModes
		{
			get { return TileSourceFactory.AvailableModes; }
		}

		/// <summary>
		/// All the "solid" modes user can choose from.
		/// </summary>
		/// <remarks>
		/// Some of the map modes can be marked as "overlay modes". These modes usually are used in combinations with other modes,
		/// for example, the StreetOverlay mode is normally used in combination with the PhotoMap mode.
		/// This property returns all the modes which are not "overlay" ones.
		/// </remarks>
		public IList<MapMode> AvailableNonOverlayModes
		{
			get
			{
				return TileSourceFactory.AvailableModes
					.Where(x => !x.IsOverlayTileSource)
					.OrderBy(x=>x.Order)
					.ThenBy(x=>x.Identifier)
					.ToList();
			}
		}

		/// <summary>
		/// All the "overlay" modes user can choose from
		/// </summary>
		/// <remarks>
		/// Some of the map modes can be marked as "overlay modes". These modes usually are used in combinations with other modes,
		/// for example, the StreetOverlay mode is normally used in combination with the PhotoMap mode.
		/// This property returns all the modes which are "overlay" ones.
		/// </remarks>
		public IList<MapMode> AvailableModes
		{
			get
			{
				return TileSourceFactory.AvailableModes
					.Where(x => x.IsOverlayTileSource)
					.OrderBy(x => x.Order)
					.ThenBy(x => x.Identifier)
					.ToList();
			}
		}

		protected virtual void ModeChanged(MapMode oldMode, MapMode newMode)
		{
			using (TileSourcesCollection.GetAtomicOperation())
			{
				var oldSource = TileSourcesCollection.FirstOrDefault(x => x.Mode == oldMode);
				if (oldSource != null)
				{
					Debug.Assert(oldSource == TileSourcesCollection[0], "'Main' mode should always be at the bottom level!");
					TileSourcesCollection.Remove(oldSource);
				}
				var tileSource = TileSourceFactory.CreateTileSource(newMode);
				if (tileSource != null)
				{
					TileSourcesCollection.Insert(0, tileSource); //insert the 'Main' mode at the very bottom level
				}
			}
		}

		public BaseTileSourceFactory TileSourceFactory { get; protected set; }

		bool _showStreets;
		public virtual bool ShowStreets
		{
			get { return _showStreets; }
			set
			{
				if (value == _showStreets) return;
				_showStreets = value;
				NotifyPropertyChanged(() => ShowStreets);
			}
		}

		private bool _isShowStreetsEnabled;
		public bool IsShowStreetsEnabled
		{
			get { return _isShowStreetsEnabled; }
			protected set
			{
				if (value == _isShowStreetsEnabled) return;
				_isShowStreetsEnabled = value;
				NotifyPropertyChanged(() =>IsShowStreetsEnabled);
			}
		}

		DateTime? _date;
		public virtual DateTime Date
		{
			get { return _date ?? DateTime.MaxValue.Date; }
			set
			{
				if (_date == value) return;
				var coercedDate = value == DateTime.MaxValue.Date ? (DateTime?)null : value;
				if (coercedDate == _date) return;
				_date = coercedDate;

				foreach (var source in TileSourcesCollection.Where(x => x.Mode.HasDates)) source.Date = _date;
				NotifyPropertyChanged(() => Date);
			}
		}

		private readonly ObservableCollection<DateTime> _dates = new ObservableCollection<DateTime>();
		protected ObservableCollection<DateTime> DatesCollection { get { return _dates; } }
		public IEnumerable<DateTime> Dates
		{
			get { return _dates; }
		}

		private bool _isTimeLineEnabled;
		public bool IsTimeLineEnabled
		{
			get { return _isTimeLineEnabled; }
			private set
			{
				if (_isTimeLineEnabled == value) return;
				_isTimeLineEnabled = value;
				NotifyPropertyChanged(() => IsTimeLineEnabled);
			}
		}


		public static readonly DependencyProperty OpacityProperty = DependencyProperty.Register("Opacity", typeof(double), typeof(BaseTileSourceProvider), new PropertyMetadata(1d));
		public double Opacity
		{
			get { return (double)GetValue(OpacityProperty); }
			set { SetValue(OpacityProperty, value); }
		}
		#endregion

		public abstract BaseTileSourceProvider Clone();

		protected virtual void FillClone(BaseTileSourceProvider clone)
		{
			clone.Mode = Mode;

			clone._tileSources = new AtomicObservableCollection<BaseTileSource>(_tileSources);

			clone.Opacity = Opacity;
			clone.ShowStreets = ShowStreets;
			clone._date = _date;
		}

		public override bool Equals(object obj)
		{
			if (obj is BaseTileSourceProvider)
				return ((BaseTileSourceProvider)obj) == this;
			return false;
		}

		public override int GetHashCode()
		{
			return string.Format("{0}{1}", TileSourceFactory.GetType().AssemblyQualifiedName, Mode.Identifier).GetHashCode();
		}

		private AtomicObservableCollection<BaseTileSource> _tileSources;
		protected AtomicObservableCollection<BaseTileSource> TileSourcesCollection { get { return _tileSources; } }
		public IEnumerable<BaseTileSource> TileSources
		{
			get { return _tileSources; }
		}

		private void OnTileSourcesCollectionChanged(object sender,  NotifyCollectionChangedEventArgs e)
		{
			var datesEnabled = TileSourcesCollection.Where(x => x.Mode.HasDates).ToList();
			IsTimeLineEnabled = datesEnabled.Count > 0;

			foreach (var source in datesEnabled) source.Date = _date;

			if (e.Action == NotifyCollectionChangedAction.Add)
			{
				foreach (BaseTileSource bts in e.NewItems)
				{
					bts.LogicalBounds = this.LogicalBounds;
					bts.MaxDataZoomLevel = this.MaxDataZoomLevel;
				}
			}

			NotifyPropertyChanged(() => TileSources);
		}

		public void AddTileSourceByMode(MapMode mode)
		{
			//this guy will do validation
			var tileSource = TileSourceFactory.CreateTileSource(mode);
			if (tileSource == null) return;

			var elementToStayBefore = TileSourcesCollection
				.Select((elem, ind) => new { elem, ind })
				.Where(x => x.elem.Mode.Order > mode.Order)
				.FirstOrDefault();

			using (TileSourcesCollection.GetAtomicOperation())
			{
				if (elementToStayBefore == null)
					TileSourcesCollection.Add(tileSource);
				else
					TileSourcesCollection.Insert(elementToStayBefore.ind, tileSource);
			}
		}

		public void RemoveTileSourceBySpecification(MapMode mode)
		{
			//don't care of mode validation, if it's not there - it is not there, nothing is removed.
			using (TileSourcesCollection.GetAtomicOperation())
			{
				TileSourcesCollection.RemoveAll(x => x.Mode == mode);
			}
		}

		#region INotifyPropertyChanged
		public event PropertyChangedEventHandler PropertyChanged;

		protected void NotifyPropertyChanged(params string[] propertyNames)
		{
			if (PropertyChanged != null)
				foreach (var propertyName in propertyNames)
					PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}

		protected virtual void NotifyPropertyChanged<T>(Expression<Func<T>> expression)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(BindingUtilities.PropertyName(expression)));
			}
		}

		#endregion

		/// <summary>
		/// This method is called externally by the map view when the map location is changed.
		/// Concrete implementations may override it when they are interested in this event.
		/// </summary>
		/// <param name="currentCentre">The current map centre location.</param>
		public virtual void OnLocationChanged(Location currentCentre) { }
	}
}